package org.tulkas.selector;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author <a href="mailto:tulkas.web@gmail.com">Tulkas</a>
 *
 */
public class Tokenizer {
	public static final String WHITESPACE = "[\\x20\\t\\r\\n\\f]";
	public static final String CHARACTERS = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+";
	public static final String IDENTIFIER = CHARACTERS.replaceAll("w", "w#");
	public static final String ATTRIBUTES = "\\[" + WHITESPACE + "*("
			+ CHARACTERS + ")" + WHITESPACE + "*(?:([*^$|!~]?=)" + WHITESPACE
			+ "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + IDENTIFIER + ")|)|)"
			+ WHITESPACE + "*\\]";

	public static final Pattern PTN_ESCAPE = Pattern
			.compile("\\\\([\\da-f]{1,6}" + WHITESPACE + "?|(" + WHITESPACE
					+ ")|.)");
	public static final Pattern PTN_COMMA = Pattern.compile(WHITESPACE + "*,"
			+ WHITESPACE + "*");
	public static final Pattern PTN_COMBINATOR = Pattern.compile(WHITESPACE
			+ "*([>+~]|" + WHITESPACE + ")" + WHITESPACE + "*");
	public static final Pattern PTN_TRIM = Pattern.compile(WHITESPACE
			+ "+|((?:^|[^\\\\])(?:\\\\.)*)" + WHITESPACE + "+");
	public static final Pattern PTN_ID = Pattern.compile("#(" + CHARACTERS
			+ ")");
	public static final Pattern PTN_CLASS = Pattern.compile("\\.(" + CHARACTERS
			+ ")");
	public static final Pattern PTN_TAG = Pattern.compile("("
			+ CHARACTERS.replaceAll("w", "w*") + ")");
	public static final Pattern PTN_ATTR = Pattern.compile(ATTRIBUTES);
	public static final Pattern PTN_PSEUDO_CHILD = Pattern
			.compile(":(only|first|last|nth|nth-last)-(child|of-type)(?:\\("
					+ WHITESPACE + "*(even|odd|(([+-]|)(\\d*)n|)" + WHITESPACE
					+ "*(?:([+-]|)" + WHITESPACE + "*(\\d+)|))" + WHITESPACE
					+ "*\\)|)");
	public static final Pattern PTN_PSEUDO_MISC = Pattern
			.compile(":("
					+ CHARACTERS
					+ ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()\\[\\]]|"
					+ ATTRIBUTES.replaceAll("3", "8") + ")*)|.*)\\)|)");

	private static final Pattern[] PATTERN_SEQUENCE = new Pattern[] {
			PTN_COMBINATOR, PTN_TAG, PTN_CLASS, PTN_ATTR, PTN_PSEUDO_CHILD,
			PTN_PSEUDO_MISC, PTN_ID };

	private FilterBuilder builder = null;

	public Tokenizer(FilterBuilder builder) {
		this.builder = builder;
	}

	public static String escape(String input) {
		String text = input;
		int start = 0;
		while (true) {
			Matcher mt = PTN_ESCAPE.matcher(text);
			if (mt.find(start)) {
				String escaped = mt.group(1);
				String replacement = null;
				try {
					int code = Integer.decode("0x" + escaped.trim());
					if (mt.group(2) == null) {
						replacement = new String(Character.toChars(code));
					} else {
						replacement = escaped;
					}
				} catch (NumberFormatException e) {
					replacement = escaped;
				}

				start = mt.start() + replacement.length();
				text = mt.replaceFirst(replacement);
			} else {
				break;
			}
		}

		return text;
	}

	public int tokenize(String selector) {
		String remain = selector;
		Matcher mt = null;
		String matched = null;
		while (remain.length() > 0) {
			matched = null;
			mt = PTN_COMMA.matcher(remain);
			if (mt.find()) {
				matched = mt.group();
				remain = remain.substring(matched.length());
				if (remain.length() > 0 && builder != null) {
					builder.nextChain();
				}
			}

			matched = null;
			for (Pattern pattern : PATTERN_SEQUENCE) {
				mt = pattern.matcher(remain);
				if (mt.find()) {
					try {
						matched = processToken(mt);
					} catch (Exception e) {
						if (builder != null) {
							throw new IllegalArgumentException(
									"Error processing token: [" + matched
											+ "], Selector: [" + selector
											+ "], Unresolved: [" + remain + "]",
									e);
						}
					}
					if (matched != null && matched.length() > 0) {
						remain = remain.substring(matched.length());
					}
				}
			}

			if (matched == null) {
				break;
			}
		}

		if (builder != null && remain.length() > 0) {
			throw new IllegalArgumentException("Illegal Selector: [" + selector
					+ "], Unresolved: [" + remain + "]");
		}

		return selector.length() - remain.length();
	}

	protected String processToken(Matcher matcher) {
		Pattern ptn = matcher.pattern();
		if (ptn == PTN_TAG) {
			if (builder != null) {
				builder.addTag(matcher.group(1));
			}
			return matcher.group(0);
		} else if (ptn == PTN_CLASS) {
			if (builder != null) {
				builder.addClass(matcher.group(1));
			}
			return matcher.group(0);
		} else if (ptn == PTN_ID) {
			if (builder != null) {
				builder.addId(matcher.group(1));
			}
			return matcher.group(0);
		} else if (ptn == PTN_COMBINATOR) {
			return processCombinator(matcher);
		} else if (ptn == PTN_ATTR) {
			return processAttr(matcher);
		} else if (ptn == PTN_PSEUDO_CHILD) {
			return processPseudoChild(matcher);
		} else if (ptn == PTN_PSEUDO_MISC) {
			return processPseudoMisc(matcher);
		}

		return null;
	}

	private String processCombinator(Matcher matcher) {
		String text = matcher.group(1);
		Matcher mt = PTN_TRIM.matcher(text);
		text = mt.replaceAll(" ");
		if (text.length() == 1) {
			if (builder != null) {
				builder.addCombinator(text.charAt(0));
			}
			return matcher.group(0);
		} else {
			if (builder != null) {
				throw new IllegalArgumentException("Illegal Combinator: ["
						+ text + "]");
			} else {
				return null;
			}
		}
	}

	private String processAttr(Matcher matcher) {
		String name = escape(matcher.group(1));
		String operator = matcher.group(2);
		String value = matcher.group(4);
		if (value == null) {
			value = matcher.group(5);
		}
		if (value != null) {
			value = escape(value);
		}
		if (builder != null) {
			builder.addAttribute(name, operator, value);
		}
		return matcher.group(0);
	}

	private int toInteger(String sign, String digit) {
		String xv = ((sign == null) ? "" : sign)
				+ ((digit == null) ? "1" : digit);
		return Integer.parseInt(xv);
	}

	private String processPseudoChild(Matcher matcher) {
		String prefix = matcher.group(1);
		String type = matcher.group(2);
		int x = 0;
		int y = 0;
		String aug = matcher.group(3);
		if ("nth".equalsIgnoreCase(prefix)) {
			if (aug == null) {
				if (builder != null) {
					throw new IllegalArgumentException(
							"Arguments are required for nth-* selectors");
				} else {
					return null;
				}
			}

			if ("even".equalsIgnoreCase(aug)) {
				x = 2;
				y = 0;
			} else if ("odd".equalsIgnoreCase(aug)) {
				x = 2;
				y = 1;
			} else {
				if (matcher.group(4) != null) {
					x = toInteger(matcher.group(5), matcher.group(6));
				}

				y = toInteger(matcher.group(7), matcher.group(8));
			}

		} else {
			if (aug != null) {
				if (builder != null) {
					throw new IllegalArgumentException(
							"Arguments are prohibited for the selector ["
									+ matcher.group() + "]");
				} else {
					return null;
				}
			}
		}
		if (builder != null) {
			builder.addPseudoChild(prefix, type, x, y);
		}
		return matcher.group(0);
	}

	private String processPseudoMisc(Matcher matcher) {
		if (PTN_PSEUDO_CHILD.matcher(matcher.group()).find()) {
			return null;
		}

		String unquoted = null;
		String value = null;

		if (matcher.group(5) == null) {
			unquoted = matcher.group(2);
		}

		String ret = matcher.group(0);
		String name = matcher.group(1);
		if (matcher.group(4) != null) {
			value = matcher.group(4);
		} else if (unquoted != null && PTN_PSEUDO_MISC.matcher(unquoted).find()) {
			Tokenizer tmp = new Tokenizer(null);
			int idx = tmp.tokenize(unquoted);
			int offset = unquoted.indexOf(')', idx) - unquoted.length();
			value = unquoted.substring(0, unquoted.length() + offset);
			ret = ret.substring(0, ret.length() + offset);
		}

		if (builder != null) {
			builder.addPseudoMisc(name, value);
		}

		return ret;
	}

	public static void main(String[] args) {
	}

}
